/*
Hermes Core: General Purpose Mobile Agent Platform
Copyright (C) 2006 UNICAM - Università degli Studi di Camerino

Group leaders: Flavio Corradini and Emanuela Merelli

Developers: Ezio Bartocci, Diego Bonura, Rosario Culmone,
Davide Lorenzoli, Leonardo Mariani, Francesca Piersigilli, 
Lorenzo Scortichini, Leonardo Vito.

GNU Lesser Public License

This library is free software; you can redistribute it 
and/or modify it under the terms of the GNU Lesser General 
Public License as published by the Free Software Foundation, 
version 2.1 of the License.

This library is distributed in the hope that it will be useful, 
but WITHOUT ANY WARRANTY; without even the implied warranty of 
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
See the GNU Library General Public License for more details.

You should have received a copy of the GNU Library General Public 
License along with this library; if not, write to the
Free Software Foundation, Inc., 59 Temple Place - Suite 330,
Boston, MA 02111-1307, USA.

Emanuela Merelli
Dipartimento di Matematica ed Informatica
Università di Camerino
via Madonna delle Carceri, 62032 Camerino, Italy
E.Mail: emanuela.merelli@unicam.it
*/
package hermesV2.core.net;

import hermesV2.PlaceAddress;
import hermesV2.util.HermesLogger;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.NumberFormatException;
import java.util.Hashtable;

/**
 * The <code>AgentClassLoader</code> resolves new agents searching classes
 * through different locations. For example system path, class path and the
 * store preference in the conf file. When the class is not found in the current
 * file system, it tries to retrive it by the connection with a place. It uses
 * the <code>SendReceive</code> component.
 * 
 * @author Diego Bonura
 * @author Davide Lorenzoli
 */
public class SmartClassLoader extends ClassLoader {
	/** contains every class found, it's useful for recursive searches */
	private static Hashtable<String, byte[]> classesNoSerialVersion = new Hashtable<String, byte[]>();

	/** DOCUMENT ME! */
	private static Hashtable<String, byte[]> classesWithSerialVersion = new Hashtable<String, byte[]>();

	/**
	 * true if the new classes found on the net must be saved in the current
	 * place (see property file) otherwise the class is downloaded, resolved but
	 * not saved
	 */
	private boolean save;

	/**
	 * is the <code>sendReceive</code> component, is used when a class must be
	 * searched on the net
	 */
	private SendReceive sendReceive;

	/**
	 * is the path where the new class downloaded from a remote place is stored
	 */
	protected String path;

	/** the file extension of the java class */
	protected String extension;

	/** the placeaddres where search new classes */
	private PlaceAddress placeAddress;

	/**
	 * Creates a new <code>AgentClassLoader</code> object.
	 * 
	 * @param path
	 *            the store path (see configuration file)
	 * @param extension
	 *            the exstension of the java class
	 * @param save
	 *            the interface of the <code>SendReceive</code> component
	 * @param sendReceive
	 *            the place address where search new classes by net
	 */
	public SmartClassLoader(String path, String extension, boolean save,
			SendReceive sendReceive) {
		this.path = path;
		this.sendReceive = sendReceive;
		this.extension = extension;
		this.save = save;
	}

	/**
	 * It tries to find the class in the store path (see Property file).
	 * 
	 * @param classFileName
	 *            String contains the className plus UID.
	 * @param resolvePackageName
	 *            true when the className is diveded in directory for example
	 *            packageName.foo -> packageName\foo false when is searched the
	 *            packageName.foo file.
	 * 
	 * @return The new class, otherwise null.
	 */
	private byte[] getClassBytes(String classFileName,
			boolean resolvePackageName) {
		byte[] result;
		if (resolvePackageName) {
			classFileName = classFileName.replace('.', System.getProperty(
					"file.separator").charAt(0))
					+ "." + extension;
		} else {
			classFileName = classFileName + "." + extension;
		}
		try {
			result = loadClassBytes(classFileName);
		} catch (Exception e) {
			result = null;
		}
		return result;
	}

	/**
	 * Searches the className in the store path.
	 * 
	 * @param classFileName
	 *            String contains the className plus UID.
	 * 
	 * @return The new class, otherwise null.
	 * 
	 * @throws IOException
	 *             Problems regarding the file system access.
	 */
	protected byte[] loadClassBytes(String classFileName) throws IOException {
		File classFile = null;
		classFile = new File(path, classFileName);
		FileInputStream fi = new FileInputStream(classFile);
		byte[] result = new byte[fi.available()];
		fi.read(result);
		fi.close();
		return result;
	}

	/**
	 * Loads the class with the specified name and force to resolve it.
	 * 
	 * @param className
	 *            String contains the className.
	 * 
	 * @return The new class, otherwise null.
	 * 
	 * @throws ClassNotFoundException
	 *             When the new class is not found.
	 */
	public Class<?> loadClass(String className) throws ClassNotFoundException {
		return (loadClass(className, true));
	}

	/**
	 * Loads a class given the classname.
	 * 
	 * @param className
	 *            String contains the className.
	 * @param resolveIt
	 *            True resolve the class false no.
	 * 
	 * @return The new class, otherwise null.
	 * 
	 * @throws ClassNotFoundException
	 *             When the new class is not found.
	 */
	protected Class<?> loadClass(String className, boolean resolveIt)
			throws ClassNotFoundException {
		Class<?> result = super.loadClass(className, resolveIt);
		if (resolveIt) {
			resolveClass(result);
		}
		return result;
	}

	/**
	 * Loads a class given the classname plus the SerialVersionUID.
	 * 
	 * @param className
	 *            String contains the className.
	 * @param UID
	 *            The universal serial versione of the class.
	 * @return The new class, otherwise null.
	 * 
	 * @throws ClassNotFoundException
	 *             When the new class is not found.
	 */
	protected Class<?> loadClass(String className, long UID)
			throws ClassNotFoundException {
		Class<?> result = super.loadClass(className + "." + UID, true);
		resolveClass(result);
		return result;
	}

	/**
	 * It searches the new class in different way, in the file system (store
	 * path) and in the net.
	 * 
	 * @param classFileName
	 *            String contains the className.
	 * 
	 * @return The new class, otherwise null.
	 * 
	 * @throws ClassNotFoundException
	 *             When the new class is not found.
	 * @throws ClassFormatError
	 *             When the class loaded is corrupted.
	 */
	protected Class<?> findClass(String classFileName)
			throws ClassNotFoundException {
		/* Ricavo le informazioni che mi servono dal ClassName */
		String UID = classFileName
				.substring(classFileName.lastIndexOf(".") + 1);
		String className = classFileName.substring(0, classFileName
				.lastIndexOf("."));
		//Checks if the UID is correct
		try {
			Long.valueOf(UID).longValue();
		} catch (NumberFormatException nfe) {
			UID = "";
			className = classFileName;
		}
		/*
		 * Cerca prima di tutto nella cache poi nel file system poi nel path
		 * store (con due metodi di ricerca differenti) ed infine via net
		 */
		byte[] classData;
		/* Prima si controlla la cache */
		Class<?> result = null;
		if (UID == "") {
			classData = (byte[]) classesNoSerialVersion.get(className);
		} else {
			classData = (byte[]) classesWithSerialVersion.get(UID);
		}
		if (classData != null) {
			/* viene eseguito il parsing, e costruito l'oggetto class */
			result = defineClass(className, classData, 0, classData.length);
		}
		if (result == null) {
			/* poi si prova col primordial class loader */
			try {
				result = super.findSystemClass(className);
			} catch (ClassNotFoundException e) {
				if (className.startsWith("java.")) {
					HermesLogger.log(HermesLogger.WARNING,
							"Dangerous class not loaded!" + className);
					throw new ClassNotFoundException();
				}
			}
		}
		/*
		 * poi si prova a caricare la classe dal path specificato...prima con la
		 * risoluzione del nome della classe in sottodirectory e poi senza
		 */
		if (result == null) {
			classData = getClassBytes(classFileName, true);
			if (classData != null) {
				/* viene eseguito il parsing, e costruito l'oggetto class */
				result = defineClass(className, classData, 0, classData.length);
				if (result == null) {
					throw new ClassFormatError();
				}
			}
		}
		/*
		 * poi si prova a caricare la classe dal path specificato...senza
		 * risoluzione del package
		 */
		if (result == null) {
			classData = getClassBytes(classFileName, false);
			if (classData != null) {
				/* Trovato */
				/* viene eseguito il parsing, e costruito l'oggetto class */
				result = defineClass(className, classData, 0, classData.length);
				if (result == null) {
					throw new ClassFormatError();
				}
			}
		}
		/* ancora non trovata la classe */
		/* il caricamento via net */
		if (result == null) {
			classData = sendReceive.getClass(classFileName, placeAddress);
			if (classData == null) {
				throw new ClassNotFoundException();
			}
			/*
			 * viene eseguito il parsing, e costruito l'oggetto class
			 */
			result = defineClass(className, classData, 0, classData.length);
			if (result == null) {
				throw new ClassFormatError();
			}

			// salvo la classe nella cache
			if (result != null) {
				if (UID == "") {
					classesNoSerialVersion.put(className, classData);
				} else {
					classesWithSerialVersion
							.put(String.valueOf(UID), classData);
				}
				/* salvo la classe se ho impostato il parametro! */
				if (save) {
					try {
						saveClass(classFileName, classData);
					} catch (IOException ioe) {
						HermesLogger
								.log(
										HermesLogger.WARNING,
										"I can't save new classes (disk space full?) (write permission?)",
										ioe);
					}
				}
			}
		}
		return result;
	}

	/**
	 * Sets the <code>PlaceAddress</code> after the creation of a new
	 * <code>SmartClassLoader</code>.
	 * 
	 * @param placeAddress
	 *            the address where searchs new class files.
	 */
	public void SetPlaceAddress(PlaceAddress placeAddress) {
		this.placeAddress = placeAddress;
	}

	/**
	 * Saves the classes found on the net in the local file system (see Property
	 * file).
	 * 
	 * @param classFileName
	 *            The name of the class file plus UID.
	 * @param classBytes
	 *            The array of the class's bytes to store in the file system.
	 * 
	 * @throws IOException
	 *             Problems to access to the file system.
	 */
	private final void saveClass(String classFileName, byte[] classBytes)
			throws IOException {
		String classFile_path;
		if (path.endsWith("/")) {
			classFile_path = path + classFileName + ".class";
		} else {
			classFile_path = path + "/" + classFileName + ".class";
		}
		OutputStream os = null;
		try {
			os = new FileOutputStream(classFile_path);
			os.write(classBytes);
		} catch (IOException ioe) {
			// code can't be saved
			throw ioe;
		} finally {
			if (os != null) {
				try {
					os.close();
				} catch (IOException ioe1) {
					HermesLogger.log(HermesLogger.WARNING, "IOException", ioe1);
				}
			}
		}
	}

	/**
	 * Returns the class stored on the local cache with the key=className.
	 * 
	 * @param key
	 *            The key that identify the element on the hastable.
	 * 
	 * @return The object searched by the key.
	 */
	public byte[] getCachedClassNoSerialVersion(String key) {
		return (byte[]) classesNoSerialVersion.get(key);
	}

	/**
	 * Returns the class stored on the local cache with the
	 * key=SerialVersionUID.
	 * 
	 * @param key
	 *            The key that identify the element on the hastable.
	 * 
	 * @return The object searched by the key.
	 */
	public byte[] getCachedClassWithSerialVersion(String key) {
		return (byte[]) classesWithSerialVersion.get(key);
	}
}